package com.google.code.sms.domain;

import org.apache.commons.lang.StringUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * A play queue is a list of media elements that are associated to a remote player.
 *
 * @author Scott Ware
 */
public class PlayQueue {

    private List<MediaElement> elements = new ArrayList<MediaElement>();
    private boolean repeatEnabled;
    private String name = "(unnamed)";
    private Status status = Status.PLAYING;
    /**
     * The index of the current song, or -1 is the end of the play queue is
     * reached. Note that both the index and the play queue size can be zero.
     */
    private int index = 0;
    /**
     * Used for undo functionality.
     */
    private List<MediaElement> elementsBackup = new ArrayList<MediaElement>();
    private int indexBackup = 0;

    /**
     * Returns the user-defined name of the play queue.
     *
     * @return The name of the play queue, or <code>null</code> if no name has
     * been assigned.
     */
    public synchronized String getName() {
        return name;
    }

    /**
     * Sets the user-defined name of the play queue.
     *
     * @param name The name of the play queue.
     */
    public synchronized void setName(String name) {
        this.name = name;
    }

    /**
     * Returns the current element in the play queue.
     *
     * @return The current element in the play queue, or <code>null</code> if no
     * current element exists.
     */
    public synchronized MediaElement getCurrentElement() {
        if (index == -1 || index == 0 && size() == 0) {
            setStatus(Status.STOPPED);
            return null;
        } else {
            MediaElement element = elements.get(index);

            // Remove element from playlist if it doesn't exist.
            if (!element.exists()) {
                elements.remove(index);
                index = Math.max(0, Math.min(index, size() - 1));
                return getCurrentElement();
            }

            return element;
        }
    }

    /**
     * Returns all elements in the play queue.
     *
     * @return All elements in the play queue.
     */
    public synchronized List<MediaElement> getElements() {
        return elements;
    }

    /**
     * Returns the element at the given index.
     *
     * @param index The index.
     * @return The element at the given index.
     * @throws IndexOutOfBoundsException If the index is out of range.
     */
    public synchronized MediaElement getElement(int index) {
        return elements.get(index);
    }

    /**
     * Skip to the next element in the play queue.
     */
    public synchronized void next() {
        index++;

        // Reached the end?
        if (index >= size()) {
            index = isRepeatEnabled() ? 0 : -1;
        }
    }

    /**
     * Returns the number of elements in the play queue.
     *
     * @return The number of elements in the play queue.
     */
    public synchronized int size() {
        return elements.size();
    }

    /**
     * Returns whether the play queue is empty.
     *
     * @return Whether the play queue is empty.
     */
    public synchronized boolean isEmpty() {
        return elements.isEmpty();
    }

    /**
     * Returns the index of the current song.
     *
     * @return The index of the current song, or -1 if the end of the play queue
     * is reached.
     */
    public synchronized int getIndex() {
        return index;
    }

    /**
     * Sets the index of the current element.
     *
     * @param index The index of the current element.
     */
    public synchronized void setIndex(int index) {
        makeBackup();
        this.index = Math.max(0, Math.min(index, size() - 1));
        setStatus(Status.PLAYING);
    }

    /**
     * Adds one or more elements to the play queue.
     *
     * @param elements The media elements to add.
     * @param index Where to add them.
     * @throws IOException If an I/O error occurs.
     */
    public synchronized void addElementsAt(Iterable<MediaElement> mediaElements, int index) throws IOException {
        makeBackup();
        for (MediaElement element : mediaElements) {
            elements.add(index, element);
            index++;
        }
        setStatus(Status.PLAYING);
    }

    /**
     * Adds one or more elements to the play queue.
     *
     * @param append     Whether existing elements in the play queue should be kept.
     * @param mediaElements The elements to add.
     * @throws IOException If an I/O error occurs.
     */
    public synchronized void addElements(boolean append, Iterable<MediaElement> mediaElements) throws IOException {
        makeBackup();
        if (!append) {
            index = 0;
            elements.clear();
        }
        for (MediaElement element : mediaElements) {
            elements.add(element);
        }
        setStatus(Status.PLAYING);
    }

    /**
     * Convenience method, equivalent to {@link #addElements(boolean, Iterable)}.
     */
    public synchronized void addElements(boolean append, MediaElement... mediaElements) throws IOException {
        addElements(append, Arrays.asList(mediaElements));
    }

    /**
     * Removes the element at the given index.
     *
     * @param index The play queue index.
     */
    public synchronized void removeElementAt(int index) {
        makeBackup();
        index = Math.max(0, Math.min(index, size() - 1));
        if (this.index > index) {
            this.index--;
        }
        elements.remove(index);

        if (index != -1) {
            this.index = Math.max(0, Math.min(this.index, size() - 1));
        }
    }

    /**
     * Clears the play queue.
     */
    public synchronized void clear() {
        makeBackup();
        elements.clear();
        index = 0;
    }

    /**
     * Shuffles the play queue.
     */
    public synchronized void shuffle() {
        makeBackup();
        MediaElement currentElement = getCurrentElement();
        Collections.shuffle(elements);
        if (currentElement != null) {
            index = elements.indexOf(currentElement);
        }
    }

    /**
     * Sorts the play queue according to the given sort order.
     */
    public synchronized void sort(final SortOrder sortOrder) {
        makeBackup();
        MediaElement currentMediaElement = getCurrentElement();

        Comparator<MediaElement> comparator = new Comparator<MediaElement>() {
            @Override
            public int compare(MediaElement a, MediaElement b) {
                switch (sortOrder) {
                    case TRACK:
                        Integer trackA = a.getMetaData().getTrackNumber();
                        Integer trackB = b.getMetaData().getTrackNumber();
                        if (trackA == null) {
                            trackA = 0;
                        }
                        if (trackB == null) {
                            trackB = 0;
                        }
                        return trackA.compareTo(trackB);

                    case ARTIST:
                        String artistA = StringUtils.trimToEmpty(a.getMetaData().getArtist());
                        String artistB = StringUtils.trimToEmpty(b.getMetaData().getArtist());
                        return artistA.compareTo(artistB);

                    case ALBUM:
                        String albumA = StringUtils.trimToEmpty(a.getMetaData().getAlbum());
                        String albumB = StringUtils.trimToEmpty(b.getMetaData().getAlbum());
                        return albumA.compareTo(albumB);
                    default:
                        return 0;
                }
            }
        };

        Collections.sort(elements, comparator);
        if (currentMediaElement != null) {
            index = elements.indexOf(currentMediaElement);
        }
    }

    /**
     * Moves the element at the given index one step up.
     *
     * @param index The play queue index.
     */
    public synchronized void moveUp(int index) {
        makeBackup();
        if (index <= 0 || index >= size()) {
            return;
        }
        Collections.swap(elements, index, index - 1);

        if (this.index == index) {
            this.index--;
        } else if (this.index == index - 1) {
            this.index++;
        }
    }

    /**
     * Moves the element at the given index one step down.
     *
     * @param index The play queue index.
     */
    public synchronized void moveDown(int index) {
        makeBackup();
        if (index < 0 || index >= size() - 1) {
            return;
        }
        Collections.swap(elements, index, index + 1);

        if (this.index == index) {
            this.index++;
        } else if (this.index == index + 1) {
            this.index--;
        }
    }

    /**
     * Returns whether the play queue is repeating.
     *
     * @return Whether the play queue is repeating.
     */
    public synchronized boolean isRepeatEnabled() {
        return repeatEnabled;
    }

    /**
     * Sets whether the play queue is repeating.
     *
     * @param repeatEnabled Whether the play queue is repeating.
     */
    public synchronized void setRepeatEnabled(boolean repeatEnabled) {
        this.repeatEnabled = repeatEnabled;
    }

    /**
     * Revert the last operation.
     */
    public synchronized void undo() {
        List<MediaElement> elementsTmp = new ArrayList<MediaElement>(elements);
        int indexTmp = index;

        index = indexBackup;
        elements = elementsBackup;

        indexBackup = indexTmp;
        elementsBackup = elementsTmp;
    }

    /**
     * Returns the play queue status.
     *
     * @return The play queue status.
     */
    public synchronized Status getStatus() {
        return status;
    }

    /**
     * Sets the play queue status.
     *
     * @param status The play queue status.
     */
    public synchronized void setStatus(Status status) {
        this.status = status;
        if (index == -1) {
            index = Math.max(0, Math.min(index, size() - 1));
        }
    }

    /**
     * Returns the total length in bytes.
     *
     * @return The total length in bytes.
     */
    public synchronized long length() {
        long length = 0;
        for (MediaElement mediaElement : elements) {
            length += mediaElement.getFileSize();
        }
        return length;
    }

    private void makeBackup() {
        elementsBackup = new ArrayList<MediaElement>(elements);
        indexBackup = index;
    }

    /**
     * Play queue status.
     */
    public enum Status {

        PLAYING,
        STOPPED
    }

    /**
     * Play queue sort order.
     */
    public enum SortOrder {

        TRACK,
        ARTIST,
        ALBUM
    }
}